═══ 1. Title Page ═══ Welcome to EDM/2 - The Electronic OS/2 Developers' Magazine!. Portions Copyright (C)1993 by Steve Luzynski. Issue #2 - April 1993. (Press 'Forward' to page.) ═══ 2. Copyright Notice and other Legal Stuff ═══ EDM/2 Copyright (C)1993 by Steve Luzynski. This publication may be freely distributed in electronic form provided that all parts are present in their original unmodified form. A reasonable fee may be charged for the physical act of distribution; no fee may be charged for the publication itself. All articles are copyrighted by their authors. No part of any article may be reproduced without permission from the original author. Neither this publication nor Steve Luzynski is affiliated with International Business Machines Corporation. OS/2 is a registered trademark of International Business Machines Corporation. Other trademarks are property of their respective owners. Any mention of a product in this publication does not constitute an endorsement or affiliation unless specifically stated in the text. ═══ 3. From the Editor ═══ Hi! and welcome again to EDM/2. This month was a big one for OS/2 developers. The new PDK CDROM, including all kinds of great OS/2 tools from IBM, was released at the incredible price of $15. Look for a new PM mail program from me soon now that I have the TCP/IP developer's kit... The other big news this month was the release of Borland C++ for OS/2. My copy came in on the 11th - about a week after I ordered it. I'm very impressed with this first offering from Borland... Borland C++ for OS/2 In fact, I'm impressed enough that I'm going to talk about it with the rest of my space. Borland C++ came in Borland's new style packaging - a slick blue & white box. Included are seven disks and 8 manuals. I installed all 28 megs (including all sample code) and sat down to read the manuals while I waited. Apparently, Borland supplied a DOS based decompressor program - my guess is that the install time will drop dramatically after they rewrite their compression program for OS/2. After the install completed, I made all the changes listed in the README file. I would suggest that anyone installing it do the same - otherwise a few things won't work quite right. I would also suggest deleting the IBM Toolkit and its associated CONFIG.SYS lines before installing to keep things from getting confused. Besides, BC comes with everything the Toolkit includes EXCEPT the SOM reference (.INF) file. Copy it out if you need it. Most of the sample code was ported directly from the toolkit. I compiled one to make sure everything worked (it did) and started on converting one of my EMX programs to BC. Converting from EMX to BC The first thing I noticed when I compiled my program under BC was a lot of invalid conversions. EMX let me get away without explicitly casting some type conversions; BC demanded casts for most of these. I also found that NULL and 0L are not the same thing under BC; this required determining what it was I really meant when I had used NULL under EMX. After fixing these two problems (which were really the only ones I had), I immediately noticed how much faster Borland is at compiling - especially when using precompiled headers. It is also possible to continue editing while BC is compiling - something I could never satisfactorily do when using Emacs as my development environment. I would have liked to have shown before and after code; unfortunately, in my excitement I didn't save the old EMX version. Rest assured that it isn't really all that difficult - I suspect many of my problems were caused by using an OS/2 1.3 book as a reference, something I have since rectified. Etc. You may have noticed that this issue is a little smaller than the last one. This came about for a number of reasons: 1. March brought about Spring Break for many college students (and teachers.) These people are among the contributors to this magazine, and many of them decided that lying on a warm Florida beach would be more fun than sitting in front of a computer writing articles. 2. At least for me, the release of Borland C++ for OS/2 meant that I was spending a lot more time programming since it suddenly became a lot easier. 3. Since I too had spring break I was late in sending out guidelines and the deadline to the authors. As much as I hate to admit it, this is the biggest reason. I promise next time I'll be earlier. But have no fear, next month we'll be right back up to speed! As always, article ideas, criticism, and compliments are welcome at sal8@po.cwru.edu. Steve Luzynski, Editor. ═══ 4. Corrections and Clarifications ═══ Corrections and Clarifications ═══ 4.1. From Raja Thiagarajan ═══ Dear Editor: First of all, thanks for the nice job you did converting my article from raw ASCII to INF. You did an excellent job of covering my complete ignorance of VIEW. As long as I'm here, I've got a couple of clarifications I'd like to make to my "Unofficial Guide to Palette Manager" article. The first is a warning: TRIDENT VIDEO CARDS *DO* ALSO SUFFER FROM THE FATAL BUG. That is, if you're running the Service Pack and the Trident driver that was released shortly afterwards, changing the palette and then moving an icon will LOCK YOUR MACHINE, exactly as with ET4000s. Also as with ET4000s, the problem goes away if you use the 2.1 beta. Thanks to James Justice for testing and verifying this. Finally, I'd also like to say that I've discovered one more video platform that supports Palette Manager: If you install the 2.1 beta on IBM's new ThinkPad 700C, you can use a 640x480x256-color mode that supports Palette Manager. Unfortunately, it suffers from the Universal, GpiBitBlt, and Scaling bugs that I documented in my article. But it sure is nice to do pretty paletted graphics while lying on your stomach! Raja Thiagarajan / sthiagar@bronze.ucs.indiana.edu / 3-23-93 ═══ 4.2. From the Editor ═══ Last month, in the last-minute rush to get my first issue out on time, I ended up forgetting to credit the authors for their articles on the title pages. Here's the complete list (and my apologies): o Larry Salomon - Questions and Answers o Steve Lacy - Advanced GPI o Raja Thaigarajan - Unofficial Guide to Palette Manager o Gavin Baker - Introduction to PM -- Part I o David Charlap - The Making of Minesweeper Again, my apologies to all who were needlessly confused. Steve Luzynski - Editor ═══ 5. This Month's Features ═══ o OS/2 Presentation Manager Drivers o Road Map for the WorkPlace Shell o Beginning Client/Server Programming:Named Pipes ═══ 5.1. OS/2 Presentation Drivers in a Nutshell ═══ OS/2 Presentation Drivers in a Nutshell Dave Raymer (dave@suite.com) ═══ 5.1.1. Introduction ═══ Historically, the general population has held the belief that computers are inherently difficult to use. One of the outgrowths of this popularly held belief has been the Graphical User Interface, or more commonly the GUI. The designers of OS/2, both on the Microsoft, and IBM side, strove to provide not only an easy to use interface, but an easy to program interface. This ease is measured by the robustness of the application programming interfaces (API's) that OS/2 supports, as well as the high degree of device independence that is provided by the display and printer subsystems. The OS/2 display and printer subsystems are built around the OS/2 Graphics Engine, and a set of executable programs collectively termed "Presentation Drivers." Presentation Drivers (PD's) are a special form of dynamic link library (DLL) that provide a predefined interface for the OS/2 Graphics Engine (PMGRE) to use in rendering. The display driver is one form of a presentation driver, and the printer/plotter driver is another. The primary difference between the two is that the display driver must provide support for a pointing device, but on the whole, the functionality of the two is essentially the same. In general, Joe and Suzi User interact with OS/2 through the WorkPlace Shell, which is an application that makes extensive use of two DLLs, PMWIN.DLL and PMGPI.DLL. These two DLLs are responsible for providing the API interface that application programmers use. In turn, PMWIN and PMGPI, use the PMGRE in a manner very similar to the shell's use of PMWIN and PMGPI. The PMGRE in turn uses the PD to produce the physical output that the user sees and interacts with, whether this output is on the video display, or a piece of paper. Figure 1 provides an annotated stack model of this simplified view of the OS/2 user interface. WorkPlace Shell <--- User interacts with PMWIN.DLL PMGPI.DLL <--- WorkPlace interacts with PMGRE.DLL <--- PMWIN/PMGPI interact with <--- produces visual output <--- displays visual output (fig. 1) ═══ 5.1.2. A Brief Overview Of The Printer Subsystem ═══ The OS/2 Print Subsystem consists of three primary components, the print manager, the print spooler, and the presentation driver. Under OS/2 version 2.x the print manager is implemented through WorkPlace Shell printer objects. The WPS printer object provides the user the ability to manage print jobs on a per printer basis, but is not the focus of this article, so little more will be said in regards to it. OS/2 provides two print spoolers, PMPRINT and PMPLOT, which differ in the fact that PMPLOT supplies a reverse vector clipping engine developed by Steve Matthews for Micrografx, Incorporated, and reimplemented by Wes Bell, also of Micrografx, for IBM, enabling HPGL plotters to produce correct output in what is primarily a raster oriented environment. The purpose of the print spooler is to build a meta-data file of an appli- cations request for hardcopy output, allowing the application to continue processing without without the need to wait for hardcopy output rendering to complete. The OS/2 print spoolers provide two types of meta-data files, the first is a result of opening the printer OD_QUEUED with the PM_Q_STD standard option. In this mode, the spooler will produce an intermediate data file that contains boundary information as well as the application supplied arguments to the rendering functions. The second format occurs when the printer is opened OD_QUEUED with the PM_Q_RAW option. This combination will produce an intermediate data file that is the raw printer data stream. However, the overall data path through the printer will remain the same. When used in conjunction with the spooler, the data is passed through the presentation driver twice. In the PM_Q_STD mode, the first pass is used to accumulate bounds and do basic error checking on the appli- ation supplied arguments, while the second pass, initiated by the spooler, will generate the hardcopy output. In the PM_Q_RAW format, the meta-data file created contains raw printer data stream, and will pass through the presentation driver unmodified on the second pass. ═══ 5.1.3. The Presentation Driver ═══ The OS/2 presentation driver is built around the concept of the dispatch table. The dispatch table belongs to the OS/2 graphics engine, and is passed to the driver at a specified point during the enable processing, so that the presentaion driver can override the default processing of certain key rendering routines. The enable processing for a presentation driver is handled through a procedure that is aliased to or called OS2_PM_DRV_ENABLE. The following snipet from a presentation driver module definition file's export section aliases a procedure "Enable" to OS2_PM_DRV_ENABLE, and places it at ordinal 200 in the driver. Placing the OS2_PM_DRV_ENABLE procedure at ordinal 200 is required. OS2_PM_DRV_ENABLE = Enable @200 The prototype for the Enable procedure, for the CSet/2(tm) compiler package, is as follows. ULONG _System Enable(ULONG SubFunc,ULONG Param1,ULONG Param2); The returned value from the enable procedure varies from subfunction to subfunction. The following table lists the subfunctions. Name Subfunction Parameters Used --------------------------------------------------------------- Fill_Logical_Device (1) Param2 Fill_Physical_Device (2) Param1 Disable_Physical_Device (4) Param1 Enable_DC_Begin (5) Param1 Disable_DC_Complete (6) Param1 Save_DC_State (7) Param1 Restore_DC_State (8) both Reset_DC_State (9) both Enable_DC_Complete (10) both Disable_DC_Begin (11) both The handling of each of these subfunctions is covered in some detail in the OS/2 Toolkit white books ( specifically the I/O Subsystem Presentation Driver reference), available from IBM, therefore only a brief summary of each will be provided here. Fill_Logical_Device --------------------- This ENABLE subfunction is called when the driver is loaded. Its purpose is to initialize the logical device block, which includes setting some bits telling the engine whether the PD needs a physical device block (PDEV) per DC and to hook the PMGRE entry points that the PD wishes/needs to support. Fill_Physical_Device ---------------------- This ENABLE subfunction is called each time a DevOpenDC is requested by the application. The purpose of this function is to create and allocate a physical device block, which should contain any information needed by the PD to perform I/O to the specified device. Typically these are things such as a Presentation Manager Anchor Block (HAB), a "private" memory heap, and a copy of the DEVOPENDATA, passed in to Enable() for this subfunction via Param1. Disable_Physical_Device ------------------------- This ENABLE subfunction is called when a physical device block is to be deleted. The major purpose of this function is to release any memory allocated in Fill_Physical_Device, and to close the pathway to the device. Enable_DC_Begin ----------------- This ENABLE subfunction is called each time a DC is created. It is also the first time the PD is presented the DC handle and is the time to allocate the internal DeviceContext. We are also presented with the corresponding physical device pointer returned from the Fill_Physical_Device subfunction. Disable_DC_Complete --------------------- This ENABLE subfunction is called when a DC has completed the disable process. The purpose of this function is to release any memory allocated for the PD's internal device context, often referred to as the "cookie." Save_DC_State --------------- This Enable subfunction is called when the engine wishes a device driver to save all DC information. This may be called multiple times and the driver should push the DDC in LIFO order. Restore_DC_State ------------------ This ENABLE subfunction is called when the engine wishes the PD to POP a saved DC, or POP to a specified DC. The following table shows the action to be taken based on the contents of Param2. Param2 Action -------- ------------------------------------------------- = 0 then error. > 0 it specifies which state to restore, and the others are lost, (if Number is 2, then the second DC is restored, the first DC is saved, and all others are lost. < 0 it specifies how many states to pop back. If the value is -1, then pop back to the first state (fig. 3) Reset_DC_State ---------------- This Enable subfunction is called to reset a the current DC, returning it to is initialization state. Enable_DC_Complete -------------------- This call informs the PD that the DC has been created by the Engine. Also it is the first time the PD receives the magic cookie returned from the Enable_DC_Begin subfunction. At this time, the PD should open the spooler if the current DC was openned OD_QUEUED. Also any metafiles or journaling is started here. Journaling is used primarily by raster devices that produce output through "banding." Even a low resolution printer, with 80 by 120 dots per inch would require a substantial amount of memory to represent an entire page of output in memory. By journaling, or recording the calls into the driver, the driver can break the physical page into smaller sections, replaying the journal file for each section. Journaling was very useful in the 1.X releases of OS/2 to handle segmentation induced limitations. However, in Version 2.0+, it can be used to prevent a presentation driver from consuming a large amount of physical memory, which may lead to thrashing. Disable_DC_Begin ------------------ This ENABLE subfunction is called before a the graphics engine begins disable processing. This allows the PD to do any final clean up of resources prior to the full disable. This is the point at which the PD ceases journaling, closes the spooler, etc. ═══ 5.1.4. Producing Hardcopy Output ═══ Once all ENABLE processing has been completed, the presentation driver is ready to begin producing output. In the handling of the Fill_Logical_Device Enable() subfunction, the PD hooks out a copy of the the PMGRE dispatch table. The heart of the OS/2 graphics engine is a dispatch routine that is used to access presentation drivers and internal PMGRE entry points via vectors. The dispatch table is an array of 32bit entries, each representing the addresses of entry points into the graphics engine. Logically, the presentation driver perform one of four actions on a given entry in the dispatch table. 1. Ignore it. 2. Copy it. 3. Hook it (replace it.) 4. Swap it (save and replace it.) The following macros are taken from my presentation driver template source code, and provide implementations of options two through four. #define COPY(Fun) \ da##Fun = (PFNL)*(pDispatchTable + (NGre##Fun & 0xffL)); #define HOOK(Fun) \ *(pDispatchTable + (NGre##Fun & 0xffL)) = (ULONG)Fun; #define SWAP(Fun) \ da##Fun = (PFNL)*(pDispatchTable + (NGre##Fun & 0xffL)); \ *(pDispatchTable + (NGre##Fun & 0xffL)) = (ULONG)Fun; Note that the above macros assume that the presentation driver source code provides local storage for SWAP'd and COPY'd vectors in local address space. In my presentation driver template source code, this storage is provided by variables with the prefix "da" . The following are functions which a driver does NOT need to hook, but should save the PMGRE vector. By calling the vector directly, the overhead of the PMGRE dispatch mechanism is avoided. These entry points are COPY'd . COPY( Convert ); COPY( SelectClipPath ); COPY( ConvertWithMatrix); The following entry points should be HOOK'd by the PD and processed completely. None of these involve PMGRE call-back, ie, your presentation driver must do all the work. HOOK( AccumulateBounds ); HOOK( ImageData ); HOOK( Bitblt ); HOOK( LockDevice ); HOOK( CreateLogColorTable ); HOOK( PolyScanline ); HOOK( DeviceCreateBitmap ); HOOK( PolyShortLine ); HOOK( DeviceDeleteBitmap ); HOOK( QueryColorData ); HOOK( DeviceGetAttributes ); HOOK( QueryColorIndex ); HOOK( DeviceQueryFonts ); HOOK( QueryDeviceBitmaps ); HOOK( DeviceSelectBitmap ); HOOK( QueryDeviceCaps ); HOOK( DeviceSetAttributes ); HOOK( QueryHardcopyCaps ); HOOK( DeviceSetDCOrigin ); HOOK( QueryLogColorTable ); HOOK( DeviceSetGlobalAttribute ); HOOK( QueryNearestColor ); HOOK( DrawLinesInPath ); HOOK( QueryRealColors ); HOOK( ErasePS ); HOOK( QueryRGBColor ); HOOK( Escape ); HOOK( RealizeColorTable ); HOOK( GetBitmapBits ); HOOK( RealizeFont ); HOOK( GetBoundsData ); HOOK( ResetBounds ); HOOK( GetCodePage ); HOOK( SetBitmapBits ); HOOK( GetCurrentPosition ); HOOK( SetCodePage ); HOOK( GetDCOrigin ); HOOK( SetLineOrigin ); HOOK( GetLineOrigin ); HOOK( SetPel ); HOOK( GetPairKerningTable ); HOOK( SetStyleRatio ); HOOK( GetPel ); HOOK( UnlockDevice ); HOOK( GetStyleRatio ); HOOK( UnrealizeColorTable ); The following entry PMGRE entry points should also be supported by the presentation driver, but may also be safely passed back to the PMGRE supplied entry points if the processing involves complex clipping, non-device fonts, etc. These entry points may be safely SWAP'd by a printer presentation driver. SWAP( Arc ); SWAP( FullArcBoundary ); SWAP( BeginArea ); SWAP( FullArcInterior ); SWAP( BoxBoth ); SWAP( NotifyClipChange ); SWAP( BoxBoundary ); SWAP( NotifyTransformChange ); SWAP( BoxInterior ); SWAP( PartialArc ); SWAP( CharString ); SWAP( PolyLine ); SWAP( CharStringPos ); SWAP( PolyMarker ); SWAP( DeviceQueryFontAttributes ); SWAP( QueryCharPositions ); SWAP( EndArea ); SWAP( QueryTextBox ); SWAP( FillPath ); SWAP( QueryWidthTable ); SWAP( FullArcBoth ); SWAP( SetArcParameters ); SWAP( SetCurrentPosition ); The internals of each of the PD supplied entry points is dependent on the physical device type (printer or display, raster or vector), the algorithms and data structures chosen, and of course the developer. It is therefore not within the scope of this article to discuss them further. However, each of the external entry points to the presentation driver, and for that matter, the PMGRE have a common parameter, that is worth discussing. The last parameter to all entry points is a 32 bit unsigned value which contains the entry point id in the low 16 bits and a set of flags in the high order 16 bits. These bits should be checked at the beginning of each entry point procedure, as they contain additional control information governing exactly what the PD should do. For example, on the first pass through the driver, the COM_DRAW bit will be clear, while the COM_BOUND bit will be set. In this case, the PD need only accumulate bounds for the operation, and need not produce any physical output. Other bits of interest are used to indicate whether or not the driver is currently within a path or area bracket. ═══ 5.1.5. A Few Final Notes ═══ For a display PD, there are several additional entry points, related to the support of mouse and text cursors. Under V2.0+ of OS/2, the display driver also has the additional overhead of the Virtual Device Driver necessary to support the multiple virtual DOS machines (MVDMs) and WINOS2. Overall, display drivers are more difficult to write than printer drivers due to performance considerations. If you are interested in creating a display Presentation Driver, I would highly recommend contacting IBM through the Developers Assistance Program (the local IBM branch office should be able to help.) OS/2 Presentation Drivers are not overly complex, neither are they simple. To successfully create a PD requires careful thought and design as well as a strong knowledge of computer graphics in general. The use of modular and structured programming techniques, along with object oriented concepts (one need not use an object language to write object oriented code) will make the development cycle far less frustating, and much more rewarding. Remember that a little extra effort in the design phase can save a great deal of recoding in the testing phase. ═══ 5.2. Road Map for the WorkPlace Shell ═══ Road Map to the Work Place Shell David Campbell (campbell@campbell.saic.com) ═══ 5.2.1. The Work Place Shell ═══ The Work Place Shell (WPS) is the new user interface introduced with OS/2 2.0. This article will begin a series of articles dedicated to explaining the operation and use of the WPS. In preparing this initial article, I was overwhelmed by the amount of material I would have liked to include. There are so many aspects of the WPS to discuss. For example, Installation and Maintenance, Programming new WPS Classes, and future enhancements. Since the amount of material is so large, I will divide the topic into 2 or 3 articles. The first article will discuss Installation and Maintenance. The second article will discuss programming issues facing developers who wish to write classes for the WPS. The third article will discuss future enhancements to the WPS. With the first article I will attempt to cover the following topics: o Evolution of the User Interface o Configuration and Maintenance of the WPS o Suggestions for Implementing the WPS ═══ 5.2.2. Evolution of the User Interface ═══ To better understand the WPS, it is necessary to be aware of the evolution of the user interface. The user interface has a very simple purpose, and that is to translate human initiated actions into computer operations and communicate these operations to the operating system. Traditionally their have been two different types of user interfaces. These include: 1. Character User Interface (CUI) 2. Graphical User Interface (GUI) Early user interfaces were CUI. That is they could only display the characters defined in the ASCII set. Examples of this type of interface are the command line interfaces provided with DOS 3.3 and early implementations of UNIX and VMS. This was limiting, but it was the only choice primarily because of 2 hardware constraints. Early CPUs did not have the processing power to manage a GUI. Also, the video controllers and monitors were unable to display the high resolution necessary to implement a GUI. The CUI evolved into the GUI. The GUI provided vastly superior capabilities. It was now possible to display on the screen different fonts, images, and other graphical data. This type of interface required more CPU processing power, but the power of CPUs was increasing and the cost of CPUs was decreasing, so it was an acceptable penalty. With most current GUIs, emphasis is still on the 'application'. That is to say, the user interacts with applications. These applications in turn interact with the data that the user requests. The philosophy of this type of system is somewhat backward. The user should interface directly with the data, and the application should be evoked implicitly to act upon the data. This approach to user-data interaction lead to the development of object oriented user interfaces. The WPS is IBM's attempt at an object oriented user interface (OOUI). With the graphical interface, it became possible to represent 'objects' using icons. IBM has stated that that the current WPS is the template of future OOUIs. These future OOUIs will be grafted onto AIX and future operating systems. Therefore, the importance of becoming proficient at using this type of interface is obvious. To summarize: o To implement an OOUI effectively, the video subsystem must be operating in graphical mode. This is necessary to display detailed images which represent objects. o An OOUI places emphasis on the data or object, whereas current CUIs and GUIs place emphasis on the application. ═══ 5.2.3. Configuration and Maintenance of the WPS ═══ The user interface used by OS/2 is loaded by the base operating system at system startup. OS/2 can be configured to load any type of user interface. By default, the OS/2 installation procedure specifies the WPS as the user interface. In the config.sys file, there is a line which reads: PROTSHELL=c:\os2\pmshell.exe This line specifies the protected mode user interface. If you want to load another user interface, this is where you would place the new execution file. Interestingly, another environment variable affects the WPS. This environment variable is 'RUNWORKPLACE'. This environment variable is also used to specify the shell. The WPS is composed of objects. An object is an instance of a class. Each object is represented by an icon. Each class has its own methods, data, and class icon. The context menu of each class is different. The settings notebook of each class is different. Interacting with objects has several advantages. The operating system and user interface only have to be familiar with the classes. Each external device is represented as an object. For example, the keyboard, the mouse, the storage devices, and the printers. Each file and directory on storage devices are also represented by objects. Finally, each process and print job is represented by an object. In this way, the operating system has a common interface to everything it interacts with, because everything it interacts with is an object. The WPS is written using the System Object Model (SOM). SOM is written to help developers create classes more quickly, and from different languages. SOM forms a class hierarchy which is shown below. SOMObject ├─ SOMClass ├─ SOMClassMgr └─ WPObject ├─ WPAbstract │ ├─ WPPrinter │ ├─ WPProgram │ └─ WPShadow ├─ WPFileSystem │ ├─ WPDataFile │ │ └─ WPProgramFile │ └─ WPFolder │ ├─ WPDesktop │ ├─ WPDrives │ └─ WPStartup └─ WPTransient ├─ WPJob └─ WPPort Note: Please note that the above class hierarchy is not complete. I have only included the most significant classes. Also note the class WPProgramFile. This class is actually derived from WPDataFile NOT WPFileSystem as most of the IBM documentation states. Due to the length of this article I will only discuss the classes which begin with WPObject. WPObject is the parent class of the 3 base classes:WPAbstract, WPFileSystem, and WPTransient. These 3 classes represent the foundation classes. Each base class is differentiated from the other base classes in the way in which the class data is maintained. Classes derived from WPAbstract classes store their instance data in the os2.ini file. Classes derived from WPFileSystem classes store their instance data as a file or directory on permanent storage. Classes derived from WPTransient classes do not store their instance data, therefore all WPTransient instance data is not maintained if the computer is restarted. This differentiation in storage types classifies WPAbstract and WPFileSystem as persistent, and WPTransient as being non-persistent. Each of these classes has some very common implementations. For example, the typical user creates a folder which contains programs and files. This implementation utilizes 3 WPS classes, WPProgram, WPDataFile and WPFolder. The folder used is an instance of WPFolder. An instance of WPFolder represents a directory on a diskette or hardrive. A folder is used as a container for other objects. Each file in the folder is an instance of WPDataFile. An instance of WPDataFile represents a physical file which exists either on a diskette or a hardrive. Each of the programs within the folder is an instance of WPProgram. Notice this is not the same class as WPProgramFile. Notice also that you do NOT want to copy the execution file into the folder. An instance of WPProgram is a reference to an execution file. This reference is used to specify the name of the execution file to execute, any parameters, the startup directory, and other information. An instance of this class typically uses the ICON resource within the execution file. To compare and contrast these 3 classes, we enumerate the settings notebook of each class. Notice the differences in the settings notebook for each class. An instance of WPProgram has 5 pages in its settings notebook. 1. Program 2. Session 3. Association 4. Window 5. General An instance of WPDataFile has 4 pages in its settings notebook. 1. Type 2. Menu 3. File 4. General An instance of WPFolder has 8 pages in its settings notebook. 1. View 2. Include 3. Sort 4. Background 5. Menu 6. File 7. Window 8. General Maintaining the WPS is critical to satisfactory use of the user interface. Due to the power and flexibility of the WPS, maintenance can be a somewhat difficult task. Fortunately, their are ways to effectively maintain the WPS to provide outstanding performance and usability. The number 1 thing is to keep your os2.ini and os2sys.ini files backed up. This can be done using 'RUN' commands in the config.sys file. You can use a 'RUN=C:\OS2\XCOPY OS2.INI C:\TEMP\OS2.INI' this will copy the os2.ini file before the WPS starts. Once the WPS starts, it opens the os2.ini file. The os2.ini file remains open until the system is shutdown. Therefore, it is impossible to make copies of the os2.ini file while the WPS is executing. Always properly shutdown the system. The WPS maintains several open files. Unexpectedly turning the computer off can have disastrous effects on the system configuration files. The next item of maintenance is concerned with maintaining the active class list, file types, file associations, and object handles. I have written several utilities in REXX to perform these functions. Information on these utilities is presented in the next section. ═══ 5.2.4. Suggestions for Implementing the WPS ═══ Learning to effectively use the WPS can be difficult. Upon initial introduction, the casual user tends to configure the WPS similar to the Microsoft Windows environment. For example, a software developer starts an editor to edit a source file. The user then saves the file and exits the editor. He then evokes the compiler to convert the source code to an executable file. He then runs the execution file. Here is an alternative. Create a folder on your desktop titled source. Within this folder create several data files representing your source code. For each of the data files, set the TYPE to either C code or REXX or whatever. Then create a program object (WPProgram) for the compiler. On the associations page of the program object, select C code or REXX. Now when you select the open menu of the data file, you should see an editor and a compiler. Now, to open the file to be edited, select the editor from the open menu. To open the file to be compiled, select the compiler from the open menu. In this way, you are interacting more with the data than with the application. For example, the data file can have 2 actions performed on it, edit and compile. We have created an open menu which has these options! I have created a set of REXX utilities for managing some of the aspects of the WPS. These utilities allow you to add a type, delete a type, enumerate all of the existing types, add an association, delete an association, enumerate all of the existing associations, get EA information, and set EA information. Several other utilities are needed, but I have not had the time to develop them. All of these utilities can be obtained via anonymous FTP from my machine: campbell.saic.com 139.121.81.146 Please feel free to contact me if you have questions concerning this article or the WPS. I believe the WPS to be one of the most significant parts of OS/2 2.x. I also believe that without better understanding and knowledge of the WPS, OS/2 2.x's future is somewhat in doubt. It would be a shame for such a wonderful system to go unnoticed. Let's not let that happen! ═══ 5.3. Beginning Client/Server Programming:Named Pipes. ═══ Beginning Client/Server Programming: Named Pipes Steve Lacy (sl31@andrew.cmu.edu) ═══ 5.3.1. Beginning Client/Server Programming: Named Pipes ═══ This article is an introduction to client server applications, and thier interface in OS/2. Throughout this article, click on "Forward" to read the next paragraph. I've assumed that you've already browsed through the supplied source files, "client.c" and "server.c" just so you can see generally whats going on in this program, even though you might not know what the actual function calls do, you can look at the names (like DosCreateNPipe) and see just what they do, in this case, creating a named pipe. ═══ 5.3.2. Introduction ═══ One of the current hip words used when talking about OS/2 is "client/server" Well, as a programmer, I know I've wondered myself, "what exactly is a Client/Server program?" Well, from my experience, its not a black and white definiton, since some people seem to define one style to be client/server whereas other poeple wouldn't consider that particular example to be a client/server application. One thing to keep in mind is that the design of client/server applications is usually more work than the programming of them. Designing a multithreaded program which uses named pipes and semaphores elegantly is quite a task, we'll be getting into those topics in later articles. For our little example, we're going to be developing a "reverse this string" client server application. Basically, we'll have a server that accepts connections on a named pipe, reads in a string, and spits that string back out onto the pipe, reversed. Note that in our example we're not using a multithreaded server. This means that if two clients are trying to connect to the same pipe at the same time, that only one of them will get through, and that the other will have to wait for the other to finish, until it can continue. One thing is known for sure, that client server applications use the following OS/2 kernel functions: Named Pipes, Threads/Processes, Semaphores, and Shared Memory. Here's a brief description of four mechanisms In this article, I don't attempt to define client/server applications, but I do show you the basics as to what does define a "typical" client/server program, if such a beast does exist. ═══ 5.3.2.1. Named Pipes ═══ A named pipe is a mechanism for controlling inter-process (or inter-thread) communication under OS/2 2.0. Named pipes share a lot of the characteristics of UNIX sockets, but in my opinion, their programming interface is a lot more user friendly, and it doesn't have as much programming overhead as UNIX sockets do. In this article, we're going to dulge into the basics of named pipes, and the OS/2 functions used to program a named pipe application. ═══ 5.3.2.2. Threads/Processes ═══ A standard OS/2 program, something that you would write with the standard library of C functions, has what you call a "single thread of execution." A multithreaded program, as you might think, has multiple threads of execution. Basically, this means that your program is running in two different places at the same time. Starting a thread has very little overhead, since all the code for you program is already in memory. The overhead in starting a thread has been compared to the overhead for calling a standard C function. Your program, or some thread of your program, can start up another thread by issuing a DosCreateThread call. Processes are basically equivalent to "programs" One program can start up another program by issuing a DosStartSession call. Remember that there is some significant overhead in loading and starting another program, even if its another copy of the program that is currently executing. When processes are started, they always start with the main() function of your code. ═══ 5.3.2.3. Semaphores ═══ A semaphore is an inter-process or inter-thread signaling mechanism. There are three types of semaphores in OS/2, you don't particularly need to know about them now, so I won't go into the details. The way semaphores work is that you can either post or wait on an open semaphore. When you're waiting, you stop waiting until someone else posts. The neat thing is that it can be anyone whatsoever, in any process, in any thread. ═══ 5.3.3. Design of the applications ═══ This section deals with the design of our application, and gives an introduction to the Named pipe OS/2 services that we'll be using in our application. We have to keep in mind that dealing with named pipes is in fact a lot like dealing with standard files, that is, files on disk. For example, the client end of our program uses only the DosOpen , DosWrite , DosRead , and DosClose system calls, the exact same calls that would be used if we were writing to a file. We distinguish a named pipe from a file by the name that we send to the system calls. The name for pipes must be of the form "\pipe\*" Generally, its a good idea to try to pick pipe names that you don't think other people will be using too, since this would create problems. If you're really paranoid, you'll do something like include the process number (which is essentially unique) in the pipe name, so you know that no one else could possibly have the same name as you. For our example, that would be just a little bit of overkill, so we're just going to be using the name "\pipe\reverse\npipe" I find it a good idea to choose pipe names so that they're of the form "\pipe\application-name\pipe-name" ═══ 5.3.3.1. Pipe Semantics. ═══ If we're going to have interprocess communication, we have to design our application so that the communication takes place properly, that all the data is transmitted, and that there are "clean" open and close operations. Browsing through the documentation, the most obvious looking function calls are: DosCreateNPipe , DosConnectNPipe , and DosDisConnectNPipe. So, we have functions to create pipes, "connect" to them (whatever that may mean) and to disconnect from them. Thinking of pipes as files, the create routine creates the pipe, the connect waits for the client to connect to the pipe that we've just connected. When the entire process has finished, we disconnect from the pipe, and we can either end our application, or continue with the connect cycle. ┌────────────────────────────────────────────────────────────┐ │Server Client Pipe status │ ├────────────────────────────────────────────────────────────┤ │... ... Nonexistant │ ├────────────────────────────────────────────────────────────┤ │DosCreateNPipe ... Created │ ├────────────────────────────────────────────────────────────┤ │DosConnectNPipe ... Blocking for │ │ connections │ ├────────────────────────────────────────────────────────────┤ │... DosOpen Opened │ ├────────────────────────────────────────────────────────────┤ │DosRead DosWrite Open │ ├────────────────────────────────────────────────────────────┤ │DosWrite DosRead Open │ ├────────────────────────────────────────────────────────────┤ │... DosClose Closing │ ├────────────────────────────────────────────────────────────┤ │... ... Closed │ ├────────────────────────────────────────────────────────────┤ │DosDisConnectNPipe ... Dosconnected (same│ │ as created state) │ └────────────────────────────────────────────────────────────┘ ═══ 5.3.3.2. Server implementation ═══ So, thats basically an outline of what our programs should do, but we still need a few little modifications. The main one being that we have to make sure that the DosDisconnectNPipe happens after the DosClose happens on the client side. The simples way to synchronize this it to add andother DosRead on the server side. What will happen is that the DosRead will fail with an End Of File (EOF) error, then we know that the pipe has defineately closed, and we can then do our DosDisConnectNPipe. The way that we know that DosRead has returned an end of file error is when it returns zero in the ulBytes field, which usually contains the number of bytes that we've read from the pipe. So, generally the code for our server looks like this: DosCreateNPipe(...); while (1) { DosConnectNPipe(...); /* Read the input data */ DosRead(...); /* This is where we figure out what the output will be */ DosWrite(...); /* Now we're reading for an end of file. */ DosRead(...); /* If we're not at the end of the file that is, number of bytes read isn't zero, then something has happened */ if (ulBytes!=0) error(); rc=DosDisConnectNPipe(...); } Other than the parameters for the functions, thats the program. The reason that we have a while(1) in our program is that after one client has finished, we want the server to reset and be able to accept more connections from other clients. ═══ 5.3.3.3. Client Implementation. ═══ The client, as mentioned before, reads and writes to the created pipe -- teating it as a file, not as a pipe. So, we open the file, write to it, read from it, and close it. Here's a brief version of the code: for (i=1;i as such: typedef struct _PRINTDEST { ULONG cb; LONG lType; PSZ pszToken; LONG lCount; PDEVOPENDATA pdopData; ULONG fl; PSZ pszPrinter; } PRINTDEST, *PPRINTDEST; This should save you a lot of time, since the DEVOPENSTRUC structure is already initialized and is disguised as a DEVOPENDATA structure. However, when you try to use the pdosData field in your call to DevOpenDC, you get a PMERR_INV_DRIVER_DATA error. This is particularly funny, since the printer specified this as the data to use, yet won't accept it if you use it. The only solution is to query the default driver data yourself using DevPostDeviceModes. But wait! You don't have the device name for the printer! Well, making the assumption that your machine doesn't have more than one printer attached to the queue you dropped the object on, you can use the SplQueryQueue function to determine the missing information. Sparing you the boring details, I'll cut-to-the-quick and will simply show you the procedure below: BOOL initPrinter(HAB habAnchor,PPRINTDEST ppdPrinter,PDEVOPENSTRUCT pdosPrinter) //------------------------------------------------------------------------- // This function will query the default driver data for the printer // specified in ppdPrinter. It is the caller's responsibility to free // the data using free(). // // Input: habAnchor - anchor block of the calling thread. // ppdPrinter - pointer to the PRINTDEST structure passed via // the DM_PRINTOBJECT message. // Output: pdosPrinter - points to the variable which received the new // DEVOPENSTRUC structure. // Returns: TRUE if successful, FALSE otherwise. //------------------------------------------------------------------------- { ULONG ulNeeded; PPRQINFO3 ppiQueue; CHAR achDriver[64]; CHAR achDevice[64]; PCHAR pchPos; *pdosPrinter=*((PDEVOPENSTRUC)(ppdPrinter->pdopData)); SplQueryQueue(NULL, pdosPrinter->pszLogAddress, 3, NULL, 0, &ulNeeded); ppiQueue=malloc(ulNeeded); if (ppiQueue==NULL) { return FALSE; } /* endif */ SplQueryQueue(NULL, pdosPrinter->pszLogAddress, 3, ppiQueue, ulNeeded, &ulNeeded); strcpy(achDriver,ppiQueue->pszDriverName); free(ppiQueue); pchPos=strchr(achDriver,'.'); if (pchPos!=NULL) { *pchPos=0; strcpy(achDevice,pchPos+1); } else { achDevice[0]=0; } /* endif */ ulNeeded=DevPostDeviceModes(habAnchor, NULL, achDriver, achDevice, ppdPrinter->pszPrinter, DPDM_QUERYJOBPROP); pdosPrinter->pdriv=malloc(ulNeeded); if (pdosPrinter->pdriv==NULL) { return FALSE; } /* endif */ DevPostDeviceModes(habAnchor, pdosPrinter->pdriv, achDriver, achDevice, ppdPrinter->pszPrinter, DPDM_QUERYJOBPROP); if ((ppdPrinter->fl & PD_JOB_PROPERTY)!=0) { DevPostDeviceModes(habAnchor, pdosPrinter->pdriv, achDriver, achDevice, NULL, DPDM_POSTJOBPROP); } /* endif */ return TRUE; } ═══ 6.1.1.3. Message Box Help ═══ Message boxes are an easy way to communicate something to the user from your PM application. However, one can only say so much in a message box, so PM allows you to add online help for each one. To correspond a particular message with a help panel, the panel resource id is specified in the 5th parameter and MB_HELP must be specified in the flags. So how do you process the help requests? The documentation states that a help hook must be used, and that (because the Help Manager also uses a help hook) it should be installed before creating the help instance so that you can receive the help messages. Unfortunately, doing this won't work because the documentation is incorrect. If you look up the entry for WinSetHook, you'll read that this installs the hook at the head of the hook chain, so (doing a few abstract calculations in your head) you can see that setting your help hook before creating the help instance results in the Help Manager's hook being positioned before yours. Calling WinSetHook after WinAssociateHelpInstance will fix this problem and your help hook will start seeing the messages. Don't forget to return FALSE if the message isn't processed, or the Help Manager's hook won't see them! As if this wasn't enough, the documentation on the help hook is incorrect also. It states that the second parameter (sMode) can have one of the following four values: o HFM_MENU o HFM_MB o HFM_WINDOW o HFM_APPLICATION but none of these constants are defined in the toolkit! Instead, sMode can have one of the following three values: o HLPM_FRAME o HLPM_WINDOW o HLPM_MENU HLPM_WINDOW and HLPM_MENU seem to correctly behave according to the documentation for HFM_WINDOW and HFM_MENU, respectively. There is no corresponding constant for HFM_MB though; instead, message box help requests have the mode HLPM_WINDOW with the topic number specifying the help panel id used in the WinMessageBox call. ═══ 6.2. Introduction to PM ═══ Introduction to PM Programming Part II Gavin Baker (demogrb@lust.latrobe.edu.au) ═══ 6.2.1. Introduction ═══ Introduction Welcome to the second installment of our exciting series on PM programming! (Well, I thought I'd better start on a high note ...) In this article, I present a simple program which takes advantage of OS/2 threads. It uses one thread to handle interacting with the user, and another thread to do all the work. It simply displays the current time in the middle of the window. It is obviously a trivial prorgam, but nonetheless serves to illustrate one possible use of threads. Basically you can have one program (process) doing more than one thing (threads) at once. A little like multi-multitasking... Anyway, in terms of the source code, a thread is just a function in your program which gets called in a special way which allows it to go off and run at the same time as the rest of the program. You can imagine how complicated things could get having to co-ordinate things, so OS/2 also provides IPC (Inter-Process Communication) functions to help you. The program uses other facets of OS/2 not discussed yet in this series (resources, and the Graphics Programming Interface [GPI]) so we will not spend too much time on them since the main focus is on threads. I have included the RC file which defines the dialog box and the menu, but I will leave off explaining resources to the next article. ═══ 6.2.2. Scope ═══ Scope I am assuming that you are a competent C programmer, and have a working knowledge of OS/2 from a user's perspective. The sample code here was produced with Borland C++ for OS/2, but should work with most other compilers. ═══ 6.2.3. Trademarks etc. ═══ Trademarks etc. Please note that any trademarks referred to in this article remain the property of their respective companies. #include ═══ 6.2.4. Processes & Threads ═══ Processes & Threads A process in OS/2 terms is just an application or program. Because OS/2 is multi-tasking, you can run multiple processes (in their own address spaces) at the same time. A thread is an execution unit within a process. It becomes clearer in this diagram: Editor's note: Due to time constraints, this diagram was not available in time for publication. It will be made available at a later date. The red shows OS/2 itself. The green represents our program, and the yellow boxes are the threads within it. You would typically have one thread to handle the user interface (the main window procedure), one to do all the work, and perhaps one for printing. Threads can have different priorities, and there are some very powerful functions to enable threads to communicate with each other, to synchronise and co-ordinate events. Threads within a process share the address space, which means the variables and functions within the program have the same scope between two threads. For example, consider these two threads: void Thread1() void Thread2() { { while (1) while (1) i++; i--; } } Get the feeling we may not be getting anywhere...? These two threads are playing a "tug-of-war" with the poor old (overused) variable i. The first thread will be forever incrementing i, and the second thread will forever be undoing all that hard work. The variable i will stay around the same value, but won't be exactly zero due to the dynamic prioritising of threads that OS/2 performs - if a Thread1 gets a tiny bit more CPU time, i will go up a little. Threads allow your programs to be much more efficient, and responsive to the user. Here I will show you how. ═══ 6.2.5. The Code ═══ The Code Right - now we'll go straight to the code. The general idea is to have the main program handle the user interactions, and during initialization we start the worker thread which carries on its merry way despite what the main thread may be doing. The way we acheive this is by creating the main window itself, and then have the second Worker thread create a dummy window (called an OBJECT WINDOW) which doesn't actually appear on screen, but gives us a window handle and a message queue to work with. This is not the only way to do this, but it is probably the simplest. The main window sends custom messages to the second dummy window on thread 2 to do all the work. Now here we specify which portions of the include files we need, and also include a few others. #define INCL_WIN #define INCL_GPI #define INCL_WINDIALOGS #include #include #include #include #include #include #include #include "step02.h" Now we need to pass messages between the two threads, so we define our own by starting at WM_USER and going up. This ensures we don't accidentally use a system-defined message. #define WM_BEGIN_PAINT WM_USER+1 #define WM_END_PAINT WM_USER+2 #define WM_ACK WM_USER+3 Using global variables is generally considered a no-no, unless it can't be avoided. It makes things easier to implement and more self-contained if you keep globals to a bare minimum. We then supply prototypes for our functions. HWND hwndMain, hwndWorker; MRESULT EXPENTRY MainWndProc (HWND, ULONG, MPARAM, MPARAM); VOID WorkerThread (); MRESULT EXPENTRY WorkWndProc (HWND, ULONG, MPARAM, MPARAM); MRESULT EXPENTRY DlgProc(HWND, ULONG, MPARAM, MPARAM); VOID WorkPaint (HWND, HPS); The main function is not very big - all it does is set up some variables, the flags for the window, initialize things for PM, then register and create our window. After we exit our main message loop (by getting a WM_QUIT) we clean up and exit. int main (void) { HAB hab; HMQ hmq; HWND hwndFrame; QMSG qmsg; ULONG flFrameFlags = FCF_TITLEBAR | FCF_SYSMENU | FCF_SIZEBORDER | FCF_MINMAX | FCF_SHELLPOSITION | FCF_TASKLIST | FCF_MENU ; randomize(); hab = WinInitialize (0); hmq = WinCreateMsgQueue (hab, 0); WinRegisterClass (hab, "STEP2", MainWndProc, CS_SIZEREDRAW, 0); hwndFrame = WinCreateStdWindow (HWND_DESKTOP, WS_VISIBLE, &flFrameFlags, "STEP2", NULL, 0, NULLHANDLE, ID_MAIN, &hwndMain); while (WinGetMsg (hab, &qmsg, 0, 0, 0)) WinDispatchMsg (hab, &qmsg); WinDestroyWindow (hwndFrame); WinDestroyMsgQueue (hmq); WinTerminate (hab); return 0; } Now here is the Window Procedure for the main window itself. The special bit to notice here is the call to _beginthread when we get created (WM_CREATE). This is where the second thread gets started. Note we just pass it the name of the function, and that function will start executing from there all by itself. It can operate more or less like any other function, with a few considerations. MRESULT EXPENTRY MainWndProc (HWND hwnd, ULONG msg, MPARAM mp1, MPARAM mp2) { FILEDLG fild; switch (msg) { case WM_CREATE: if (_beginthread (WorkerThread, 8192, NULL) == -1) { WinMessageBox (HWND_DESKTOP, hwnd, "Creation of second thread failed!", "Step 2", 0, MB_OK | MB_CUACRITICAL); return 0; } return 0; Here is the first user message we get. This message will be sent by the second thread's window procedure to the main one to say it is all setup and ready to go. We respond by telling it to go ahead and start painting the window. case WM_ACK: WinPostMsg(hwndWorker, WM_BEGIN_PAINT, 0, 0); return 0; When the main window gets resized, we stop the second thread from painting, and then restart it, causing it to update itself with the new size of the window. Note that we use WinSendMsg first, then WinPostMsg. We send the message first, which calls the target window procedure directly and will not continue until it returns (thus ensuring the message gets processed) and then we Post the message to restart in the second thread's message queue so it can keep going. case WM_SIZE: WinSendMsg(hwndWorker, WM_END_PAINT, 0, 0); WinPostMsg(hwndWorker, WM_BEGIN_PAINT, 0, 0); return 0; We get this message when the user drops a font or colour onto the window. We Invalidate the entire window and force a repaint so we can display with the new font/colours. case WM_PRESPARAMCHANGED: WinInvalidateRect(hwndMain,NULL,TRUE); return 0; If the main window needs to be painted, we simply make sure that the second thread gets on with it. Note again the different use of sending and posting the message. case WM_PAINT: WinSendMsg(hwndWorker, WM_END_PAINT, 0, 0); WinPostMsg(hwndWorker, WM_BEGIN_PAINT, 0, 0); return 0; In order to simplify things, PM does some of the painting for us if we like - returning true from this message makes PM erase the entire window with the default colour. case WM_ERASEBACKGROUND: return (MRESULT) TRUE; Now, whenever a user selects something from the menu, we get a WM_COMMAND message. We can then use the SHORT1FROMMP macro (which means extract a short value from the high word of a message parameter) to get the ID of the menu item selected. The first menu selection we process is the most important one - the About box. We call WinDlgBox which does a lot of work for us. It loads the dialog from the resource (check out STEP02.RC), runs the dialog procedure we specify (which works just like a window procedure) and will not return until the dialog is closed. This is called a modal dialog - it will not allow the user to select anything else until they finish with the dialog. Contrast this with a modeless dialog, which can be used at the same time as any other windows. These are often used for floating toolboxes, etc. We pass WinDlgBox the parent window (which will be the desktop), the owner window (our main window), a pointer to the dialog procedure which handles the messages, a handle to the module where the resource is located (by specifying NULLHANDLE OS/2 looks in the EXE file), and any extra info we want to pass the dialog procedure. case WM_COMMAND: switch (SHORT1FROMMP(mp1)) { case ID_ABOUT: WinDlgBox(HWND_DESKTOP,hwnd,(PFNWP)DlgProc,NULLHANDLE, DLG_ABOUT,NULL); return 0; Now we will use one of OS/2's standard dialogs - the File Open dialog. All we do is set up a structure with the appropriate options, and call WinFileDlg. Once it returns we can examine the FILEDLG struct for the file the user selected. This example only displays the dialog - it does nothing with what the user selected. case ID_FILEOPEN: memset(&fild, 0, sizeof(FILEDLG)); fild.cbSize=sizeof(FILEDLG); fild.fl=FDS_OPEN_DIALOG | FDS_CENTER ; fild.pszIDrive="C:"; WinFileDlg(HWND_DESKTOP,hwnd,&fild); return 0; If the user selects Exit from the File menu, we just send ourselves a WM_CLOSE message, which by default will shut down our application. case ID_FILEEXIT: WinPostMsg(hwnd, WM_CLOSE, 0, 0); return 0; Any other messages we don't need to worry about, so let PM handle them by passing them on to the default PM window procedure. default: return WinDefWindowProc(hwnd,msg,mp1,mp2); } Notice how we first destroy the second thread so it can clean up, before we close ourselves. case WM_DESTROY: WinSendMsg(hwndWorker, WM_DESTROY, mp1, mp2); return 0; } return WinDefWindowProc (hwnd, msg, mp1, mp2); } Now we get to the interesting bit - our Worker thread. It looks like a normal function, it just gets called differently. Although this example does not cover it, you must keep in mind that having multiple threads in the one program requires some forethought. You can't have two threads trying to write to the one data file, for example. We will explore this problem and how to solve it (using Semaphores) in a later article. VOID WorkerThread () { HAB hab; HMQ hmq; HWND hwndObject, hwndParent; QMSG qmsg; This should look familiar - it looks very much like the main procedure. The only difference is that we specify HWND_OBJECT when we create the window. We need our own message queue so we can talk to the main window. Notice how we ACKnowledge the main window once we get created, and go into our message loop. All the work is actually done in the second thread's window procedure. One special thing to note is the call to _endthread at the end. This lets the C runtime library clean up after us, and shuts down the thread properly. hab = WinInitialize (0); hmq = WinCreateMsgQueue(hab, 0); WinRegisterClass(hab, "STEP2_B", WorkWndProc, 0, 0); hwndWorker = WinCreateWindow( HWND_OBJECT, "STEP2_B", "", 0, 0, 0, 0, 0, HWND_OBJECT, HWND_BOTTOM, 0, NULL, NULL ); WinSendMsg( hwndMain, WM_ACK, 0, 0 ); while( WinGetMsg ( hab, &qmsg, 0, 0, 0 )) WinDispatchMsg ( hab, &qmsg ); WinPostMsg( hwndMain, WM_QUIT, 0, 0 ); WinDestroyWindow( hwndWorker ); WinDestroyMsgQueue( hmq ); WinTerminate (hab); _endthread (); } This is the dialog procedure for the About box. It is just the same as a window procedure except that for the messages we don't process, we call WinDefDlgProc instead of WinDefWindowProc because certain things have to be handled differently. If the user presses a button in the dialog, we get a WM_COMMAND much the same as if they selected a menu item. We know that the OK button is the only button that will do anything so we don't bother checking and just close the dialog by calling WinDismissDlg. We pass it the handle of the dialog, and a BOOLean value which will be returned to the calling function (back at WinDlgBox) so we can tell if the user pressed OK or Cancel. MRESULT EXPENTRY DlgProc(HWND hwnd, ULONG msg, MPARAM mp1, MPARAM mp2) { switch (msg) { case WM_COMMAND: WinDismissDlg(hwnd,TRUE); return 0; default: return WinDefDlgProc(hwnd,msg,mp1,mp2); } } This is the window procedure for the "dummy" OBJECT window which the second thread creates. We have to set up a few things when we get CREATEd. Firstly, we ask PM for a timer. We give it a handle to the anchor block (which we get from our handle), the handle itself, an ID for the timer (you can have more than one), and the delay in milliseconds between timer "ticks". We ask it to send us a WM_TIMER once a second, so we can update our clock. The next thing we do is get ourselves a Presentation Space (PS), which is like a handle which is used when we want to draw or paint in the window. We then say that we want the background to be cleared when we draw things. Otherwise the time we display would overwrite itself and soon become garbled. I will not go into to much detail on the drawing side of things, as the GPI (Graphics Programming Interface) itself could fill a book (and has). MRESULT EXPENTRY WorkWndProc(HWND hwnd, ULONG msg, MPARAM mp1, MPARAM mp2) { static BOOL Paint=FALSE; static HPS hps; SIZEL sizel; switch (msg) { case WM_CREATE: if (!WinStartTimer(WinQueryAnchorBlock(hwnd), hwnd, 1, 1000)) WinMessageBox(HWND_DESKTOP,hwnd,"Could not start timer!", "Error",0,MB_CUACRITICAL | MB_OK); hps = WinGetPS(hwndMain); GpiSetBackMix(hps, BM_OVERPAINT); return 0; WM_BEGIN_PAINT is our first user message sent to us from the main window. All we do is set a flag saying it is OK to keep painting. If we get WM_END_PAINT then we stop painting for the moment (even while we are still getting WM_TIMER messages). case WM_BEGIN_PAINT: Paint = TRUE; return 0; case WM_END_PAINT: Paint = FALSE; return 0; Every second, we will get a WM_TIMER message, and all we do is check if it is Ok to paint, and then do so. case WM_TIMER: if (Paint) WorkPaint(hwndMain, hps); return 0; If we get closed, make sure we clean up by stopping our timer and releasing the PS we got to draw with. Cleanup is always very important, and could potentially cause some nasty bugs which may not be obvious. Always consult the PM Reference manual for functions you may use which require resources to be released or destroyed. case WM_DESTROY: WinStopTimer(WinQueryAnchorBlock(hwnd), hwnd, 0); WinReleasePS(hps); return 0; } return WinDefWindowProc (hwnd, msg, mp1, mp2); } This function does the painting for us. We get the size of the main window (a RECTangLe), then calculate its width and height. VOID WorkPaint (HWND hwnd, HPS hps) { ULONG x, y, cx, cy; RECTL rect; POINTL ptl; char s[42]; struct time t; WinQueryWindowRect(hwnd, &rect); cx = rect.xRight - rect.xLeft; cy = rect.yTop - rect.yBottom; We check what the current time is, and compose our string. gettime(&t); sprintf(s,"Current Time: %2.2d:%2.2d%colon.%2.2d",t.ti_hour,t.ti_min,t.ti_sec); We want the time to be shown roughly in the middle of the screen, so we figure out where that is. We then randomly pick a colour and display the string at the specified point. ptl.x=(cx/3); ptl.y=(cy/2); GpiSetColor(hps, rand() % 16); GpiCharStringAt(hps, &ptl, strlen(s), s); } ═══ 6.2.6. Prologue ═══ Prologue Well, that's it. It's not a particularly exciting program, and it could probably be more efficient, but it does provide a general skeleton for a multi-threaded PM application. Study the structure of the program, in blocks. The main function which sets up and creates the window, the main window procedure which handles all the messages and delegates work through user messages to the worker thread. Then the worker thread which sets up the object window, and its corresponding window procedure which does all the work. Try a few things. First, drop a different font on the time. Then try a colour (this will only last until it updates itself, since it changes colour itself). Then bring up the About box and move it so you can see the time still updating itself. Something we have not discussed is thread states. Basically, a thread can either be running (the currently executing thread of which there can only ever be one), ready to run, or blocked (waiting for something to happen). This makes for very efficient programming. Imagine a terminal program. One thread can be handling the user interface, one doing general work, and another monitoring the serial port for input. It would call DosRead from COM1 for example. If there is nothing to read, it doesn't have to sit in a loop and poll the port. OS/2 will block the thread and drop its priority until DosRead returns with something, so it won't get any CPU time until it needs it. The possibilities for threads are endless - background recalculating, background printing, etc. are just some. See how threads could improve your programs. ═══ 6.2.7. What Next? ═══ What Next? There is a lot of ground to cover in this area, and we have only scratched the surface. But fear not - help is at hand! (You'll just have to wait until next month...) I welcome any feedback on this article (netmail preferred) - any comments or suggestions you may have, questions on this article, or things you would like to see in a future article. I hope you have learned something! ═══ 6.2.8. Bibliography ═══ Bibliography The following references were used in the preparation of this article: ■ Borland C++ for OS/2 Documentation ■ OS/2 Version 2.0 - The Redbooks ■ OS/2 Version 2.0 Technical Library ═══ 7. Future Attractions ═══ Coming up in the future, we have: o Introduction to PM Part 3 o Writing Installable File Systems o Getting Started with IPF o And much more! ═══ 8. Contributors to this issue ═══ o Gavin R Baker o David Campbell o Steve Lacy o Steve Luzynski o Dave Raymer o Larry Salomon ═══ 8.1. Gavin R Baker ═══ Gavin R Baker Gavin R Baker is the man behind ThinkSoft, a consulting firm based in Melbourne Australia which specialises in developing custom software. He has experience in Assembler, Pascal, C, C++, (and a lot of other languages), and has worked with Unix, DOS, Windows, OS/2, VMS and Pick operating systems. He is an active member of Team OS/2. When he isn't programming, he is also a musician, an actor, and wastes lots of time reading Net News. He can be contacted thusly: net: demogrb@lust.latrobe.edu.au cis: 100026,270 bix: gbaker ═══ 8.2. David Campbell ═══ David has a degree in Mechanical Engineering. Currently he is working as a network designer and software developer. He is involved in everything from wide area token ring networks to developing an object oriented eMail system which works with IBM's TCP/IP for OS/2. David W. Campbell campbell@campbell.saic.com Work : (615) 481-2131 Home : (615) 693-1479 ═══ 8.3. Steve Lacy ═══ Steve Lacy is a third year computer science student at Carnegie Mellon University, and is currently overoccupied with school, and his job as an undergraduate research programmer for the Digital Mapping Lab at CMU. You can reach Steve via e-mail at sl31@andrew.cmu.edu. He would be glad to hear any and all of your comments about the previous article. Steve also spends his free time collecting CD's, and unfortunately spends far too much money in the process. Remember, diversity is knowledge, and personal gratification overrides the need for food in many cases..... ═══ 8.4. Steve Luzynski ═══ Steve Luzynski is the editor and creator of this magazine. He is currently a Computer Engineering student at Case Western Reserve University in Cleveland, OH, where he spends a lot of time being cold. Steve has yet to release any programs for OS/2 as a direct result of: 1) editing this magazine; and 2) having to waste time going to class when there are programs to write. Steve can by reached via e-mail at 'sal8@po.cwru.edu' or on Compuserve at 72677,2140. The old fashioned kind of mail can currently find Steve at: Steve Luzynski 11904 Carlton Road, Apt. 430D Cleveland, OH 44106 ═══ 8.5. Dave Raymer ═══ Dave lives in Fort Worth, Texas with his wife, two sons, and five dogs. His current employer is Suite Software, a small privately held company which produces an object oriented, multi-platform distributed operating system. He has worked extensively in Windows/DOS, and OS/2; both at the device and application levels. Additionally he has worked in X-Windows/UNIX and NeXTSTEP, as well as in MVS and VMS. Dave can be reached in the following ways. Please do not hesitate to contact him for any reason. He is available for Q&A issues (preferably through e-mail.) e-mail: Internet -- dave@suite.com Prodigy -- shph80a voice/direct: Work -- (214)980-9900 Home -- 6528 Levitt Drive Watauga, Texas, 76148 (817)656-3813 ═══ 8.6. Larry Salomon ═══ Larry Salomon wrote his first Presentation Manager application for OS/2 version 1.1 in 1989. Since that time, he has written numerous VIO and PM applications, including the Scramble applet included with OS/2 and the I-Brow/Magnify/Screen Capture trio included with the IBM Professional Developers Kit CD-ROM currently being distributed by IBM. Currently, he works for International Masters Publishers in Stamford, Connecticut and resides in Bellerose, New York with his wife Lisa.